package felix.rule.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import felix.rule.data.entity.RelationEntity;
import felix.rule.data.entity.RuleChainEntity;
import felix.rule.data.entity.RuleNodeEntity;
import felix.rule.data.mapper.RelationMapper;
import felix.rule.data.mapper.RuleChainMapper;
import felix.rule.data.mapper.RuleNodeMapper;
import felix.rule.data.msg.EntityRelation;
import felix.rule.data.msg.RelationType;
import felix.rule.data.msg.RuleChain;
import felix.rule.data.msg.RuleNode;
import felix.rule.data.rule.NodeConnectionInfo;
import felix.rule.data.rule.RuleChainConnectionInfo;
import felix.rule.data.rule.RuleChainMetaData;
import felix.rule.service.RuleChainService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
@Transactional
public class BaseRuleChainService implements RuleChainService {

    @Autowired
    private RuleChainMapper ruleChainMapper;

    @Autowired
    private RuleNodeMapper ruleNodeMapper;

    @Autowired
    protected RelationMapper relationMapper;

    @Override
    public RuleChain findRuleChain(String ruleId) {
        RuleChainEntity ruleChainEntity = ruleChainMapper.selectOne(Wrappers.<RuleChainEntity>lambdaQuery()
                .eq(RuleChainEntity::getRuleId, ruleId)
        );
        if (ruleChainEntity == null) {
            return null;
        } else {
            return ruleChainEntity.toData();
        }
    }

    @Override
    public RuleChain findRuleChainById(String ruleChainId) {
        RuleChainEntity ruleChainEntity = ruleChainMapper.selectOne(Wrappers.<RuleChainEntity>lambdaQuery()
                .eq(RuleChainEntity::getId, ruleChainId)
        );
        if (ruleChainEntity == null) {
            return null;
        } else {
            return ruleChainEntity.toData();
        }
    }

    @Override
    public List<RuleNode> getRuleChainNodes(String ruleChainId) {
        List<EntityRelation> relations = getRuleChainToNodeRelations(ruleChainId, RelationType.RULE_CHAIN);
        List<RuleNode> ruleNodes = new ArrayList<>();
        for (EntityRelation relation : relations) {
            RuleNodeEntity ruleNodeEntity = ruleNodeMapper.selectById(relation.getToId());
            if (ruleNodeEntity != null) {
                ruleNodes.add(ruleNodeEntity.toData());
            } else {
                relationMapper.delete(Wrappers.<RelationEntity>lambdaUpdate()
                        .eq(RelationEntity::getFromId, relation.getFromId())
                        .eq(RelationEntity::getToId, relation.getToId())
                        .eq(RelationEntity::getEntityType, relation.getEntityType())
                        .eq(RelationEntity::getRelationType, relation.getRelationType())
                );
            }
        }
        return ruleNodes;
    }

    @Override
    public List<EntityRelation> getRuleNodeRelations(String ruleNodeId) {
        List<EntityRelation> relations = getRuleChainToNodeRelations(ruleNodeId, RelationType.RULE_NODE);
        List<EntityRelation> validRelations = new ArrayList<>();
        for (EntityRelation relation : relations) {
            boolean valid = true;
            String entityType = relation.getEntityType();
            if (EntityRelation.NODE_TYPE.equals(entityType) || EntityRelation.CHAIN_TYPE.equals(entityType)) {
                Integer integer;
                if (EntityRelation.NODE_TYPE.equals(entityType)) {
                    integer = ruleNodeMapper.selectCount(Wrappers.<RuleNodeEntity>lambdaQuery()
                            .eq(RuleNodeEntity::getId, relation.getToId()));
                } else {
                    integer = ruleChainMapper.selectCount(Wrappers.<RuleChainEntity>lambdaQuery()
                            .eq(RuleChainEntity::getId, relation.getToId()));
                }
                if (integer == 0) {
                    relationMapper.delete(Wrappers.<RelationEntity>lambdaUpdate()
                            .eq(RelationEntity::getFromId, relation.getFromId())
                            .eq(RelationEntity::getToId, relation.getToId())
                            .eq(RelationEntity::getEntityType, relation.getEntityType())
                            .eq(RelationEntity::getRelationType, relation.getRelationType())
                    );
                    valid = false;
                }
            }
            if (valid) {
                validRelations.add(relation);
            }
        }
        return validRelations;
    }

    @Override
    public RuleNode findRuleNodeById(String ruleNodeId) {
        RuleNodeEntity ruleNodeEntity = ruleNodeMapper.selectById(ruleNodeId);
        return ruleNodeEntity.toData();
    }

    @Override
    public List<String> findRuleIds() {
        List<RuleChainEntity> ruleChainEntities = ruleChainMapper.selectList(null);
        return ruleChainEntities.stream().map(RuleChainEntity::getRuleId).collect(Collectors.toList());
    }

    @Override
    public Integer findCountRuleById(String ruleId) {
        return ruleChainMapper.selectCount(Wrappers.<RuleChainEntity>lambdaQuery()
                .eq(RuleChainEntity::getRuleId, ruleId));
    }

    @Override
    public RuleChainMetaData loadRuleChainMetaData(String ruleChainId) {
        RuleChain ruleChain = findRuleChainById(ruleChainId);
        if (ruleChain == null) {
            return null;
        }
        RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
        ruleChainMetaData.setRuleId(ruleChain.getRuleId());
        ruleChainMetaData.setRuleChainId(ruleChainId);
        List<RuleNode> ruleNodes = getRuleChainNodes(ruleChainId);
        Map<String, Integer> ruleNodeIndexMap = new HashMap<>();
        for (RuleNode node : ruleNodes) {
            ruleNodeIndexMap.put(node.getId(), ruleNodes.indexOf(node));
        }
        ruleChainMetaData.setNodes(ruleNodes);
        if (ruleChain.getFirstRuleNodeId() != null) {
            ruleChainMetaData.setFirstNodeIndex(ruleNodeIndexMap.get(ruleChain.getFirstRuleNodeId()));
        }
        for (RuleNode node : ruleNodes) {
            int fromIndex = ruleNodeIndexMap.get(node.getId());
            List<EntityRelation> nodeRelations = getRuleNodeRelations(node.getId());
            for (EntityRelation nodeRelation : nodeRelations) {
                String type = nodeRelation.getType();
                if (EntityRelation.NODE_TYPE.equals(nodeRelation.getEntityType())) {
                    int toIndex = ruleNodeIndexMap.get(nodeRelation.getToId());
                    ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
                } else if (EntityRelation.CHAIN_TYPE.equals(nodeRelation.getEntityType())) {
                    ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, nodeRelation.getToId(), type, nodeRelation.getAdditionalInfo());
                }
            }
        }
        return ruleChainMetaData;
    }

    @Override
    public boolean saveRuleChain(RuleChain ruleChain) {
        boolean created = false;
        RuleChainEntity ruleChainEntity = ruleChainMapper.selectOne(Wrappers.<RuleChainEntity>lambdaQuery()
                .eq(RuleChainEntity::getRuleId, ruleChain.getRuleId())
        );
        if (ruleChainEntity == null) {
            ruleChainEntity = new RuleChainEntity(ruleChain);
            ruleChainMapper.insert(ruleChainEntity);
            created = true;
        } else {
            RuleChainEntity update = new RuleChainEntity(ruleChain);
            update.setId(ruleChainEntity.getId());
            ruleChainMapper.updateById(update);
        }
        ruleChain.setId(ruleChainEntity.getId());
        if (ruleChain.getId() == null) {
            createRelation(new EntityRelation(ruleChain.getRuleId(), ruleChain.getId(),
                    EntityRelation.CONTAINS_TYPE, EntityRelation.CHAIN_TYPE, RelationType.RULE_CHAIN));
        }
        return created;
    }

    @Override
    public RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMetaData) {
        RuleChain ruleChain = findRuleChain(ruleChainMetaData.getRuleId());
        if (ruleChain == null) {
            ruleChain = findRuleChainById(ruleChainMetaData.getRuleChainId());
            if (ruleChain == null) {
                return null;
            }
        } else {
            ruleChainMetaData.setRuleChainId(ruleChain.getId());
        }

        List<RuleNode> nodes = ruleChainMetaData.getNodes();
        List<RuleNode> toAddOrUpdate = new ArrayList<>();
        List<RuleNode> toDelete = new ArrayList<>();

        Map<String, Integer> ruleNodeIndexMap = new HashMap<>();
        if (nodes != null) {
            for (RuleNode node : nodes) {
                if (node.getId() != null) {
                    ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node));
                } else {
                    toAddOrUpdate.add(node);
                }
            }
        } else {
            nodes = new ArrayList<>();
        }

        List<RuleNode> existingRuleNodes = getRuleChainNodes(ruleChainMetaData.getRuleChainId());
        for (RuleNode existingNode : existingRuleNodes) {
            deleteEntityRelations(existingNode.getId());
            Integer index = ruleNodeIndexMap.get(existingNode.getId());
            if (index != null) {
                toAddOrUpdate.add(ruleChainMetaData.getNodes().get(index));
            } else {
                toDelete.add(existingNode);
            }
        }
        for (RuleNode node : toAddOrUpdate) {
            node.setRuleChainId(ruleChain.getId());
            RuleNodeEntity ruleNodeEntity = new RuleNodeEntity(node);
            ruleNodeMapper.insert(ruleNodeEntity);
            node.setId(ruleNodeEntity.getId());
            createRelation(new EntityRelation(ruleChainMetaData.getRuleChainId(), node.getId(),
                    EntityRelation.CONTAINS_TYPE, EntityRelation.NODE_TYPE, RelationType.RULE_CHAIN));
            int index = nodes.indexOf(node);
            nodes.set(index, node);
            ruleNodeIndexMap.put(node.getId(), index);
        }
        for (RuleNode node : toDelete) {
            deleteRuleNode(node.getId());
        }
        String firstRuleNodeId = null;
        if (ruleChainMetaData.getFirstNodeIndex() != null && !nodes.isEmpty()) {
            firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId();
        }
        if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId))
                || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
            ruleChain.setFirstRuleNodeId(firstRuleNodeId);
            RuleChainEntity ruleChainEntity = new RuleChainEntity(ruleChain);
            ruleChainMapper.updateById(ruleChainEntity);
        }
        if (ruleChainMetaData.getConnections() != null) {
            for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
                String from = nodes.get(nodeConnection.getFromIndex()).getId();
                String to = nodes.get(nodeConnection.getToIndex()).getId();
                String type = nodeConnection.getType();
                createRelation(new EntityRelation(from, to, type, EntityRelation.NODE_TYPE, RelationType.RULE_NODE));
            }
        }
        if (ruleChainMetaData.getRuleChainConnections() != null) {
            for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
                String from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId();
                String to = nodeToRuleChainConnection.getTargetRuleChainId();
                String type = nodeToRuleChainConnection.getType();
                createRelation(new EntityRelation(from, to, type, EntityRelation.CHAIN_TYPE, RelationType.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
            }
        }

        return loadRuleChainMetaData(ruleChainMetaData.getRuleChainId());
    }

    private void deleteRuleNode(String id) {
        ruleNodeMapper.deleteById(id);
    }

    private void deleteEntityRelations(String id) {
        relationMapper.delete(Wrappers.<RelationEntity>lambdaUpdate()
                .eq(RelationEntity::getFromId, id)
        );
    }

    private void createRelation(EntityRelation rule) {
        RelationEntity relationEntity = relationMapper.selectOne(Wrappers.<RelationEntity>lambdaQuery()
                .eq(RelationEntity::getFromId, rule.getFromId())
                .eq(RelationEntity::getToId, rule.getToId())
                .eq(RelationEntity::getRelationType, rule.getRelationType())
                .eq(RelationEntity::getEntityType, rule.getEntityType())
        );
        if (relationEntity == null) {
            relationMapper.insert(new RelationEntity(rule));
        } else {
            RelationEntity update = new RelationEntity(rule);
            relationMapper.update(update, Wrappers.<RelationEntity>lambdaUpdate()
                    .eq(RelationEntity::getFromId, rule.getFromId())
                    .eq(RelationEntity::getToId, rule.getToId())
                    .eq(RelationEntity::getRelationType, rule.getRelationType())
                    .eq(RelationEntity::getEntityType, rule.getEntityType())
            );
        }
    }

    @Override
    public IPage<RuleChain> findRuleChain(IPage<RuleChainEntity> page, String ruleId, String name) {
        LambdaQueryWrapper<RuleChainEntity> wrapper = Wrappers.lambdaQuery();
        if (!StringUtils.isEmpty(ruleId)) {
            wrapper.eq(RuleChainEntity::getRuleId, ruleId);
        }
        if (!StringUtils.isEmpty(name)) {
            wrapper.like(RuleChainEntity::getName, name);
        }
        IPage<RuleChainEntity> entityPage = ruleChainMapper.selectPage(page, wrapper);
        return entityPage.convert(RuleChainEntity::toData);
    }

    @Override
    public void deleteRuleChainById(String ruleChainId) {
        List<EntityRelation> nodeRelations = getRuleChainToNodeRelations(ruleChainId, RelationType.RULE_CHAIN);
        for (EntityRelation relation : nodeRelations) {
            deleteRuleNode(relation.getToId());
        }
        deleteEntityRelations(ruleChainId);
        ruleChainMapper.deleteById(ruleChainId);
    }

    @Override
    public List<RuleNode> getReferencingRuleChainNodes(String ruleId, String ruleChainId) {
        // 用于规则节点到规则链的更新，查询出当前规则链被哪个节点引用了。
        List<RelationEntity> relationEntities = relationMapper.selectList(Wrappers.<RelationEntity>lambdaQuery()
                .eq(RelationEntity::getToId, ruleChainId)
                .eq(RelationEntity::getRelationType, RelationType.RULE_NODE));
        List<RuleNode> ruleNodes = new ArrayList<>();
        for (RelationEntity relation : relationEntities) {
            RuleNodeEntity ruleNodeEntity = ruleNodeMapper.selectById(relation.getFromId());
            if (ruleNodeEntity != null) {
                ruleNodes.add(ruleNodeEntity.toData());
            }
        }
        return ruleNodes;

    }

    private List<EntityRelation> getRuleChainToNodeRelations(String ruleChainId, RelationType relationType) {
        LambdaQueryWrapper<RelationEntity> wrapper = Wrappers.<RelationEntity>lambdaQuery()
                .eq(RelationEntity::getFromId, ruleChainId)
                .eq(RelationEntity::getRelationType, relationType);
        List<RelationEntity> relationEntities = relationMapper.selectList(wrapper);
        return relationEntities.stream().map(RelationEntity::toData).collect(Collectors.toList());
    }
}
